﻿using System;
using System.Threading;
using Microsoft.SPOT;


using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using GTI = Gadgeteer.Interfaces;

namespace Gadgeteer.Modules.Seeed
{
    /// <summary>
    /// A TemperatureHumidity module for Microsoft .NET Gadgeteer
    /// </summary>
    public class TemperatureHumidity : GTM.Module
    {
        private GTI.DigitalIO _data;
        private GTI.DigitalOutput _sck;

        private bool _measurementRequested = false;
        private bool _continuousMeasurement = false;

        // Note: A constructor summary is auto-generated by the doc builder.
        /// <summary></summary>
        /// <param name="socketNumber">The socket that this module is plugged in to.</param>
        public TemperatureHumidity(int socketNumber)
        {
            // This finds the Socket instance from the user-specified socket number.  
            // This will generate user-friendly error messages if the socket is invalid.
            // If there is more than one socket on this module, then instead of "null" for the last parameter, 
            // put text that identifies the socket to the user (e.g. "S" if there is a socket type S)
            Socket socket = Socket.GetSocket(socketNumber, true, this, null);

            _sck = new GTI.DigitalOutput(socket, Socket.Pin.Four, false, this);
            _data = new GTI.DigitalIO(socket, Socket.Pin.Three, true, GTI.GlitchFilterMode.Off, GTI.ResistorMode.Disabled, this);

            new Thread(TakeMeasurements).Start();
        }


        private void TakeMeasurements()
        {
            double temperatureReading;
            int rawRHReading;
            double rhReading;

            // Wait for SHT10 sensor to start-up and get to sleep mode
            Thread.Sleep(11);

            while (true)
            {
                if (_measurementRequested || _continuousMeasurement)
                {
                    _measurementRequested = false;

                    temperatureReading = 0;
                    rhReading = 0;
                    rawRHReading = 0;

                    ResetCommuncation();

                    SHT_TransmissionStart();
                    temperatureReading = TranslateTemperature(BoolArrayToInt(SHT_MeasureTemperature(), false));

                    SHT_TransmissionStart();
                    rawRHReading = BoolArrayToInt(SHT_MeasureRH(), false);
                    rhReading = TranslateRH(rawRHReading);

                    // Correct RH reading based on temperature
                    rhReading = (temperatureReading - 25) * (0.01 + 0.00008 * rawRHReading) + rhReading;

                    temperatureReading = System.Math.Round(100.0 * temperatureReading) / 100.0;
                    rhReading = System.Math.Round(100.0 * rhReading) / 100.0;

                    // Throw event
                    OnMeasurementCompleteEvent(this, temperatureReading, rhReading);

                    // Wait at least 3.6 seconds before next reading to prevent sensor from warming up
                    Thread.Sleep(3600);
                }

                Thread.Sleep(100);
            }
        }


        /// <summary>
        /// Begins a sensor measurement and raises the <see cref="MeasurementComplete"/> event when done.
        /// </summary>
        /// <remarks>
        /// To prevent the sensor from overheating, there is approximately a 3.6 second wait after each measurement.
        /// If you call this method repeatedly in a shorter time span, your requests are not queued. 
        /// Use the <see cref="StartContinuousMeasurements"/> method to obtain continuous measurements.
        /// </remarks>
        public void RequestMeasurement()
        {
            _measurementRequested = true;
        }

        /// <summary>
        /// Starts continuous measurements.
        /// </summary>
        /// <remarks>
        /// <para>
        ///  When you call this method, the <see cref="TemperatureHumidity"/> sensor takes a measurement, 
        ///  raises the <see cref="MeasurementComplete"/> event when the measurement is complete, and repeats.
        ///  To stop continuous measurements, call the <see cref="StopContinuousMeasurements"/> method.
        /// </para>
        /// <note>
        ///  To prevent the sensor from overheating, there is approximately a 3.6 second wait between measurements.
        /// </note>
        /// </remarks>
        public void StartContinuousMeasurements()
        {
            _continuousMeasurement = true;
        }

        /// <summary>
        /// Stops continuous measurements.
        /// </summary>
        /// <remarks>
        /// When you call the <see cref="StartContinuousMeasurements"/> method, the <see cref="TemperatureHumidity"/> sensor takes a measurement, 
        /// raises the <see cref="MeasurementComplete"/> event when the measurement is complete, and repeats.
        /// Call this method to stop continuous measurements.
        /// </remarks>
        public void StopContinuousMeasurements()
        {
            _continuousMeasurement = false;
        }

        private void SHT_TransmissionStart()
        {
            _data.Write(true);

            _sck.Write(true);
            _data.Write(false);
            _sck.Write(false);
            _sck.Write(true);
            _data.Write(true);
            _sck.Write(false);
        }

        private bool[] SHT_MeasureTemperature()
        {

            bool ack;
            bool[] reading = new bool[16];

            _data.Write(false);

            // 3 x SCK pulses: Address 000
            _sck.Write(true);   //A2 : 0
            _sck.Write(false);  //

            _sck.Write(true);   //A1 : 0
            _sck.Write(false);  //

            _sck.Write(true);   //A2 : 0
            _sck.Write(false);  //

            // Command = 00011
            _sck.Write(true);  //C4 : 0
            _sck.Write(false); //

            _sck.Write(true);  //C3 : 0
            _sck.Write(false); //

            _sck.Write(true);  //C0 : 0
            _sck.Write(false);

            _data.Write(true); //C1 : 1
            _sck.Write(true);
            _sck.Write(false);
            _data.Write(false);

            _data.Write(true); //C0 : 1
            _sck.Write(true);


            _sck.Write(false); // Complete SCK clock

            // ACK : DATA should have been pulled low by the sensor, until next SCK falling edge
            _sck.Write(true);
            ack = _data.Read();

            // Complete 9th SCK Clock. Sensor should release DATA line (and pulled-up by internal pull-up)
            _sck.Write(false);

            //TEMP
            if (ack) ErrorPrint("Communication failure when trying to read temperature.");

            //It will take up to 80ms to read. Sensor will
            // pull DATA line low when ready
            // TODO make this blocking (interrupt?)
            while (_data.Read())
            {
                Thread.Sleep(10);
            }

            // Read first byte in
            for (short i = 0; i < 8; i++)
            {

                reading[i] = _data.Read();
                _sck.Write(true);
                _sck.Write(false);
            }

            //  ACKnoledge receipt of first byte
            _data.Write(false);

            // ACK Clock

            _sck.Write(true);
            _sck.Write(false);

            // Read second byte in
            for (short i = 8; i < 16; i++)
            {

                reading[i] = _data.Read();
                _sck.Write(true);
                _sck.Write(false);
            }

            //  Skip CRC by keeping ACK high
            _data.Write(true);

            return reading;
        }

        private bool[] SHT_MeasureRH()
        {

            bool ack;
            bool[] reading = new bool[16];

            _data.Write(false);

            // 3 x SCK pulses: Address 000
            _sck.Write(true);   //A2 : 0
            _sck.Write(false);  //

            _sck.Write(true);   //A1 : 0
            _sck.Write(false);  //

            _sck.Write(true);   //A2 : 0
            _sck.Write(false);  //

            // Command = 00101
            _sck.Write(true);  //C4 : 0
            _sck.Write(false); //

            _sck.Write(true);  //C3 : 0
            _sck.Write(false); //

            _data.Write(true); //C2 : 1
            _sck.Write(true);
            _sck.Write(false);
            _data.Write(false);

            _sck.Write(true);  //C1 : 0
            _sck.Write(false);

            _data.Write(true); //C0 : 1
            _sck.Write(true);

            _sck.Write(false); // Complete SCK clock

            // ACK : DATA should have been pulled low by the sensor, until next SCK falling edge
            _sck.Write(true);
            ack = _data.Read();

            // Complete 9th SCK Clock. Sensor should release DATA line (and pulled-up by internal pull-up)
            _sck.Write(false);


            //It will take up to 80ms to read. Sensor will
            // pull DATA line low when ready
            // TODO: make this use a blocking call (e.g. an interrupt?)
            while (_data.Read())
            {
                Thread.Sleep(10);
            }

            // Read first byte in
            for (short i = 0; i < 8; i++)
            {
                reading[i] = _data.Read();
                _sck.Write(true);
                _sck.Write(false);
            }

            //  ACKnoledge receipt of first byte
            _data.Write(false);

            // ACK Clock
            _sck.Write(true);
            _sck.Write(false);

            // Read second byte in
            for (short i = 8; i < 16; i++)
            {
                reading[i] = _data.Read();
                _sck.Write(true);
                _sck.Write(false);
            }

            //  Skip CRC by keeping ACK high
            _data.Write(true);

            return reading;

        }

        private void ResetCommuncation()
        {

            //if (!_data.Active) _data.Active = true;
            _data.Write(true);

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

            _sck.Write(true);
            _sck.Write(false);  //0

        }

        private double TranslateRH(int rawRH)
        {
            return -2.0468 + 0.0367 * rawRH - 1.5955E-6 * rawRH * rawRH;
        }

        private double TranslateTemperature(int rawTemp)
        {
            return -39.65 + 0.01 * rawTemp;
        }

        /// <summary>
        /// Returns the integer value represented by an array of Boolean values.
        /// </summary>
        /// <param name="boolArray">The array to get the integer value of.</param>
        /// <param name="zeroIndexLeastSignificant">
        ///  <b>true</b> if the first index of the array (0) holds the least significant portion
        ///  of the value represented by the entire array; otherwise, <b>false</b>.
        /// </param>
        /// <returns>The integer value represented by <paramref name="boolArray"/></returns>
        /// <remarks>
        /// This method treats <paramref name="boolArray"/> as a binary representation of an integer value.
        /// When <paramref name="zeroIndexLeastSignificant"/> is <b>true</b>, the first index of <paramref name="boolArray"/> (0)
        /// holds the least significant bit, the second index (1) holds the next significant bit, and so on. 
        /// When <paramref name="zeroIndexLeastSignificant"/> is <b>false</b>, this is reversed; the last index of <paramref name="boolArray"/>
        /// holds the least significant bit, the second to the last index holds the next significant bit, and so on.
        /// </remarks>
        private int BoolArrayToInt(bool[] boolArray, bool zeroIndexLeastSignificant)
        {
            int temp = 0;

            for (int i = 0; i < boolArray.Length; i++)
            {
                if (boolArray[i]) temp += 1 << (zeroIndexLeastSignificant ? i : boolArray.Length - 1 - i);
            }

            return temp;
        }


        /// <summary>
        /// Represents the delegate that is used for the <see cref="MeasurementComplete"/>event.
        /// </summary>
        /// <param name="sender">The <see cref="TemperatureHumidity"/> object that raised the event.</param>
        /// <param name="temperature">The measured temperature.</param>
        /// <param name="relativeHumidity">The measured humidity.</param>
        public delegate void MeasurementCompleteEventHandler(TemperatureHumidity sender, double temperature, double relativeHumidity);

        /// <summary>
        /// Raised when a temperature and humidity measurement is complete.
        /// </summary>
        public event MeasurementCompleteEventHandler MeasurementComplete;

        private MeasurementCompleteEventHandler _OnMeasurementComplete;

        /// <summary>
        /// Raises the <see cref="MeasurementComplete"/>event.
        /// </summary>
        /// <param name="sender">The <see cref="TemperatureHumidity"/> object that raised the event.</param>
        /// <param name="temperature">The measured temperature.</param>
        /// <param name="relativeHumidity">The measured humidity.</param>
        protected virtual void OnMeasurementCompleteEvent(TemperatureHumidity sender, double temperature, double relativeHumidity)
        {
            if (_OnMeasurementComplete == null) _OnMeasurementComplete = new MeasurementCompleteEventHandler(OnMeasurementCompleteEvent);
            if (Program.CheckAndInvoke(MeasurementComplete, _OnMeasurementComplete, sender, temperature, relativeHumidity))
            {
                MeasurementComplete(sender, temperature, relativeHumidity);
            }
        }
    }
}
